package ru.papergames.battleserver.model.battle;

import org.apache.log4j.Logger;
import ru.common.util.MTRandom;
import ru.common.util.Point3D;
import ru.papergames.Config;
import ru.papergames.battleserver.model.battle.map.BattleMap;
import ru.papergames.battleserver.model.battle.map.Cell;
import ru.papergames.battleserver.model.battle.map.LinesToBeRemovedInfo;
import ru.papergames.battleserver.network.WebServerClient;
import ru.papergames.battleserver.network.serverpackets.flashuser.*;
import ru.papergames.common.enums.BattleType;

import java.util.*;

/**
 *
 * TODO: replace all RuntimeExceptions with proper handling
 *
 * @author: atroshanin
 * Date: 09.04.2010
 * Time: 22:52:55
 */
public abstract class TurnBasedBattle extends AbstractBattle {

    private static final Logger log = Logger.getLogger(TurnBasedBattle.class.getName());

    private BattlePlayersSequenceController sequenceController = new BattlePlayersSequenceController();

    /**
     * this lock is for locking parallel access to actions from soldiers of one BattlePlayer
     * Because two different players can not access battle actions(move, attack, etc)
     */
//    private Lock syncActionLock = new ReentrantLock();

    private Random rand = MTRandom.getInstance().getSecureRandom();

//    private boolean turnProceeds;
    private long turnTimeLength;
    private long currentTurnStartTime;
    private long currentTurnFinishTime;
    private long currentLastSoldierActTimeFinish;
    private BattlePlayer playerMovingNow;

    public TurnBasedBattle(int battleOrderId, WebServerClient wsClient, int maxPcMembersCount, int maxNpcMembersCount, int teamsCount, BattleMap battleMap) {
        super(battleOrderId, wsClient, maxPcMembersCount, maxNpcMembersCount, teamsCount, battleMap);
        turnTimeLength = 20 * 1000;
    }

    // *********************************************************************
    // ***********************Getters & Setters*****************************
    // *********************************************************************

    public boolean isPlayerMovingNow(BattlePlayer player) {
        return player != null && player == playerMovingNow;
    }

    public BattlePlayer getPlayerMovingNow() {
        return playerMovingNow;
    }

    private boolean addMembersPossible() {
        return battleState == BattleState.INIT || battleType == BattleType.ASSAULT;
    }

    public void setPlayerMovingNow(BattlePlayer playerMovingNow) {
        this.playerMovingNow = playerMovingNow;
    }

    public void setTurnTimeLength(long turnTimeLength) {
        this.turnTimeLength = turnTimeLength;
    }

    // *********************************************************************
    // ****************Methods for initializing/Starting battle*************
    // *********************************************************************

    public void addBattleMember(BattlePlayer member, boolean join) {
        super.addBattleMember(member, join);
        if (join) {
            // sequenceController.reInitAfterJoin(battleMembers, member);
        }
    }

    protected void arrangeMemberTurns() {
        sequenceController.init(battleMembers);
    }

    protected void onAllClientsLoaded() {
        playerMovingNow = sequenceController.getBattlePlayerForNextTurn();
        super.onAllClientsLoaded();
    }

    // *********************************************************************
    // ******Methods for managing battle(chane turn, finish, etc)***********
    // *********************************************************************

    public boolean isBattleTurnProceeds() {
        return System.currentTimeMillis() < currentTurnFinishTime;
    }

    public void finishPlayerTurn() {
        if (playerMovingNow != null && !winnerFound) {
            if (!playerMovingNow.isPcPlayer()) {
                /*for (BattleSoldier npcSoldier : playerMovingNow.getSoldiers()) {
                    NpcActionStateContainer npcActState = npcSoldier.getNpcActionState();
                    if (npcActState != null) {
                        npcActState.clearState();
                    }
                }*/
            }
            playerMovingNow.sendPacket(BattleYourTurnEnds.STATIC_PACKET);
            broadcastToBattleMembers(new BattlePlayerFinishedTurn(playerMovingNow.getId()));
        }
    }

    public boolean choseNextPlayerForTurn() {

        if (winnerFound) {
            finishBattle();
            return false;
        }

        if (playerMovingNow == null) {
            finishBattle();
            return false;
        }

        if (battleNpcMembersMap.size() > 0) {
            // check if all PC players are online
            boolean havePcPlayersConnected = false;
            for (BattlePlayer pcBattleMember : battlePcMembersMap.values()) {
                if (pcBattleMember.isConnected()) {
                    havePcPlayersConnected = true;
                    break;
                }
            }
            if (!havePcPlayersConnected) {
                finishBattle();
                return false;
            }
        }

        playerMovingNow = sequenceController.getBattlePlayerForNextTurn();
        if (playerMovingNow != null) {
            return true;
        }
        finishBattle();
        return false;
    }

    public void startCurrentTurn() {

        if (battleMap.getFreeCellsCount() < Config.NEW_BALLS_COUNT) {
            finishBattle();
            return;
        }

        List<GameBall> newBalls = newBallsThrower.throwNewBalls(this, battleMap);

        playerMovingNow.recalculateActionPoints();
        playerMovingNow.setLastActionFinishTime(1);
        playerMovingNow.sendPacket(new BattlePlayerActionPointsChanged(playerMovingNow));

        currentTurnStartTime = System.currentTimeMillis();
        if (playerMovingNow.isConnected()) {
            currentTurnFinishTime = currentTurnStartTime + turnTimeLength;
        } else {
            System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++");
            System.out.println("Player is disconnected or has no AP, skipping his turn");
            System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++");
            currentTurnFinishTime = currentTurnStartTime + 200;
        }
        playerMovingNow.setLastActionFinishTime(currentTurnStartTime + Config.TIME_THROW_NEW_BALLS);
        broadcastToBattleMembers(new BattleNewBallsThrown(this, newBalls));
        playerMovingNow.sendPacket(BattleYourTurn.STATIC_PACKET);
        broadcastToBattleMembers(new BattlePlayerStartedTurn(playerMovingNow.getId()));
    }

    // *********************************************************************
    // **Methods for user actions while battle is running(move, atack, etc)*
    // *********************************************************************

    /**
     * Method for all battle actions that MUST NEVER be called by two threads at same time(move, attack, etc)
     * Now(from 2010.04.27) we determine that all actions might be synchronized because of concurrent access to battle
     * instance from TurnControllerThread. TurnController and this method may change time of ending turn,
     * so we synchrinize it
     * @param player - battle player
     * @param act - action
     * @param args - action parameters
     */
    public void executeSynchronizedAction(BattlePlayer player, BattlePlayerAction act, Object [] args) {
        if (!isBattleRunning()) {
            return;
        }
        if (!isBattleTurnProceeds()) {
            player.sendPacket(new SystemMessage(SystemMessageId.TURN_TIME_FINISHED_WAIT_FOR_NOTIFICATION));
            return;
        }
        if (player.getActionPointsLeft() <= 0) {
            player.sendPacket(new SystemMessage(SystemMessageId.NO_ACTION_POINTS));
            return;
        }
        if (player.isLastActionStillProceeds()) {
            player.sendPacket(new SystemMessage(SystemMessageId.LAST_ACTION_STILL_PROCEEDS));
            return;
        }
        if (player == playerMovingNow) {
            if (actionLock.tryLock()) {
                try {
                    if (player == playerMovingNow) {
                        long curTime = System.currentTimeMillis();
                        long actTime = executeSynchronizedActionInternal(player, act, args);
                        player.setLastActionFinishTime(curTime + actTime);
                        if (currentLastSoldierActTimeFinish < player.getLastActionFinishTime()) {
                            currentLastSoldierActTimeFinish = player.getLastActionFinishTime();
                        }
                        if (actTime > 0) {
                            if (winnerFound || !(player.getActionPointsLeft() > 0)) {
                                currentTurnFinishTime = curTime + actTime;
                            } else {
                                if (curTime + actTime > currentTurnFinishTime) {
                                    player.setActionPointsLeft(0);
//                                    log.info("changing finish time. Time was: " + currentTurnFinishTime);
                                    currentTurnFinishTime = curTime + actTime;
//                                    log.info("changing finish time. Time new: " + currentTurnFinishTime);
                                }
                            }
                        }
                    } else {
                        player.sendPacket(new SystemMessage(SystemMessageId.NOT_YOUR_TURN));
                    }
                } finally {
                    if (player.isPcPlayer()) {
                        player.allowNpcActions();
                    }
                    actionLock.unlock();
                }

            } else {
                if (player.isPcPlayer()) {
                    player.disallowNpcActions();
                }
                player.sendPacket(NpcConcurrentActionGoing.STATIC_PACKET);
            }
        } else {
            player.sendPacket(new SystemMessage(SystemMessageId.NOT_YOUR_TURN));
        }
    }

    /**
     * This method executes action and time that might be spend for processing this action
     * @param player - player that makes this actino
     * @param act - action(MOVE, ATACK etc)
     * @param args - aruments for action. For each action arguments are different
     * @return time that might be spend for processing this action
     */
    private long executeSynchronizedActionInternal(BattlePlayer player, BattlePlayerAction act, Object [] args) {
        long actTime = 0;
        switch (act) {
            case MOVE_BALL: {
                if (args != null && args.length == 1 &&
                        args[0] instanceof List) {
                    List<Point3D> route = (List<Point3D>) args[0];
                    actTime = moveBall(player, route);
                    //tryFinishBattle();
                }
                break;
            }
            case USE_BONUS: {
                if (args != null && args.length == 2 &&
                        args[0] instanceof Integer && args[1] instanceof Integer ) {
//                    Integer x = (Integer) args[0];
//                    Integer y = (Integer) args[1];
//                    actTime = attackCell(player, soldier, x == null ? 0 : x, y == null ? 0 : y);
                    // TODO: implement it
                    actTime = 500;
                }
                break;
            }
            case FORCE_TURN_FINISH: {
                long newFinishTime = System.currentTimeMillis() + 200;
                if (newFinishTime > currentLastSoldierActTimeFinish) {
                    currentTurnFinishTime = currentTurnFinishTime > newFinishTime ? newFinishTime : currentTurnFinishTime;
                }
                break;
            }
        }
        if (findAndRemoveLines(player)) {
            actTime = actTime + Config.TIME_REMOVE_LINES;
        }
        return actTime;
    }

    private long moveBall(BattlePlayer player, List<Point3D> route) {
        if (route.size() < 2) {
            player.sendPacket(BattleIncorrectRoute.STATIC_PACKET);
            return 0;
        }
        Point3D srcPoint = route.get(0);
        GameBall ball = battleMap.getGameBall(srcPoint);
        if (ball == null) {
            player.sendPacket(BattleBallNotFound.STATIC_PACKET);
            return 0;
        }
        if (ball.getColor() != player.getBallColor()) {
            player.sendPacket(BattleNotYourBall.STATIC_PACKET);
            return 0;
        }
        if (!checkRoute(route.subList(1, route.size() - 1))) {
            player.sendPacket(BattleIncorrectRoute.STATIC_PACKET);
            return 0;
        }
        Point3D dstPoint = route.get(route.size() - 1);
        battleMap.moveGameBall(srcPoint, dstPoint);

        broadcastToBattleMembers(new BattlePlayerMovedBall(player, route, ball));

        player.decreaseActionPoints(Config.AP_MOVE_BALL, BattlePlayerAction.MOVE_BALL);
        return Config.TIME_MOVE_BALL * route.size();
    }

    private boolean checkRoute(List<Point3D> route) {
        // todo: Check непрерывность
        for (Point3D point : route) {
            if (!battleMap.isCellFree(point)) {
                return false;
            }
        }
        return true;
    }

    private boolean findAndRemoveLines(BattlePlayer player) {

        LinesToBeRemovedInfo linesToBeRemovedInfo = linesFinder.findCellsToBeFree(battleMap, playerMovingNow.getBallColor());

        Set<Cell> cellsToBeFree = linesToBeRemovedInfo.getCellsInLines();
        if (cellsToBeFree.size() > 0) {
            for (Cell cell : cellsToBeFree) {
                battleMap.removeGameBall(cell);
            }

            player.getStatistics().addRemovedLines(linesToBeRemovedInfo.getLinesSizes());

            broadcastToBattleMembers(new BattleRemoveBallsInLines(this, linesToBeRemovedInfo, playerMovingNow.getBallColor()));
            return true;
        }
        return false;
    }

}
